跳到主要内容

Go 语言学习-反射

一般的编程语言都内置了反射工具,Go 虽然是面向过程的语言,但是也提供了这个工具包

静态类型与动态类型

变量中的类型,可以分为静态类型和具体类型(动态类型)

反射取得的就是这个 pair

func main() {
var a string
// pair<statictype:string, value: "张三">
a = "张三"

var aty interface{}
// pair<type:string, value: "张三">
aty = a

str, _ := aty.(string)
fmt.Println(str)
}

静态类型

所谓的静态类型(即 static type),就是变量声明的时候的类型。(在编码时,肉眼可见的类型)

var age int   // int 是静态类型
var name string // string 也是静态类型

动态类型

所谓的 动态类型(即 concrete type,也叫具体类型)是 程序运行时系统才能看见的类型。例如空接口这种,什么 int 呀,string 呀,都可以接收。

var i interface{}   

i = 18
i = "张三"

第一行:我们在给 i 声明了 interface{} 类型,所以 i 的静态类型就是 interface{}

第二行:当我们给变量 i 赋一个 int 类型的值时,它的静态类型还是 interface{},这是不会变的,但是它的动态类型此时变成了 int 类型。

第三行:当我们给变量 i 赋一个 string 类型的值时,它的静态类型还是 interface{},它还是不会变,但是它的动态类型此时又变成了 string 类型。

反射的常用方法

这里介绍下常用的反射方法

reflect.Elem()

reflect.Elem() 函数,通过反射获取指针指向的元素类型

注意:这里侧重的是类型信息,即那些字段名,类型之类的

Go 语言程序中对指针获取反射对象时,可以通过 reflect.Elem() 方法获取这个指针指向的元素类型。这个获取过程被称为取元素,等效于对指针类型变量做了一个 * 操作,代码如下:

package main

import (
"fmt"
"reflect"
)

func main() {

// 声明一个空结构体
type cat struct {
}

// 创建cat的实例
ins := &cat{}

// 获取结构体实例的反射类型对象
typeOfCat := reflect.TypeOf(ins)

// 显示反射类型对象的名称和种类
fmt.Printf("name:'%v' kind:'%v'\n",typeOfCat.Name(), typeOfCat.Kind())
// 输出指针变量的类型名称和种类。
// Go 语言的反射中对所有指针变量的种类都是 Ptr,但注意,指针变量的类型名称是空,不是 *cat。

// 取类型的元素
typeOfCat = typeOfCat.Elem()
// 取指针类型的元素类型,也就是 cat 类型。这个操作不可逆,不可以通过一个非指针类型获取它的指针类型。

// 显示反射类型对象的名称和种类
fmt.Printf("element name: '%v', element kind: '%v'\n", typeOfCat.Name(), typeOfCat.Kind())

}

输出内容:

name: ''  kind: 'ptr'
element name: 'cat', element kind: 'struct'

示例2

type Book struct {
Id int
Title string
Price float32
Authors []string
}

// Main function
func main() {
book := Book{}

//use of Elem() method
e := reflect.ValueOf(&book).Elem()

for i := 0; i < e.NumField(); i++ {
varName := e.Type().Field(i).Name
varType := e.Type().Field(i).Type
varValue := e.Field(i).Interface()
fmt.Printf("%v %v %v\n", varName, varType, varValue)
}
}

输出内容:

Id int 0
Title string
Price float32 0
Authors []string []

reflect.Value ⭐

Go 语言中,使用 reflect.ValueOf() 函数获得值的反射值对象(reflect.Value)

value := reflect.ValueOf(rawValue)

从反射值对象获取被包装的值

可以通过下面几种方法从反射值对象 reflect.Value 中获取原值,如下表所示。

方法名说明
Interface() interface {}将值以 interface{} 类型返回,可以通过类型断言转换为指定类型
Int() int64将值以 int 类型返回,所有有符号整型均可以此方式返回
Uint() uint64将值以 uint 类型返回,所有无符号整型均可以此方式返回
Float() float64将值以双精度(float64)类型返回,所有浮点数(float32、float64)均可以此方式返回
Bool() bool将值以 bool 类型返回
Bytes() []bytes将值以字节数组 []bytes 类型返回
String() string将值以字符串类型返回

反射包 reflect 实例

上文说到,变量里面有两个内部的熟悉,一个是 type 一个是 value,可以通过反射取得这两个值

import (
"fmt"
"reflect"
)

func reflectNum(arg interface{}) {
fmt.Println("type: ", reflect.TypeOf(arg))
fmt.Println("value: ", reflect.ValueOf(arg))
}

func main() {
reflectNum(100)
}

通过反射取得方法和字段

// go不能通过反射修改私有变量,也不能通过反射调用私有方法。
type User struct {
Id int
Name string
Age int
}

// 注意这里是 User 类型 不是 *User 类型,在结构体当中,接收者类型的区别将影响该结构体方法的可见性。
func (re User) Call() {
fmt.Println("User is called...")
fmt.Printf("%v \n", re)
}

func main() {
user := User{1, "Aceld", 18}
DoFiledAndMethod(user)
}

func DoFiledAndMethod(input interface{}) {
// 获取基本的类型和 value
inputType := reflect.TypeOf(input) // 返回 Type 类型
fmt.Println("inputType is :", inputType.Name())
inputValue := reflect.ValueOf(input)
fmt.Println("inputValue is :", inputValue)
fmt.Println("===============================")

// 通过 type 取得里面的字段
for i := 0; i < inputType.NumField(); i++ {
field := inputType.Field(i)
value := inputValue.Field(i).Interface()
fmt.Println(field.Name, value)
}

fmt.Println("===============================")
// 通过 type 取得里面的方法并调用
for i := 0; i < inputType.NumMethod(); i++ {
m := inputType.Method(i)
fmt.Println(m.Name, m.Type)
}
}

注意,只有值方法才能返回数目,指针方法不统计

使用 Tag 标签

这个标签和 Java 中的注解作用是一样的,都是反射时获取

type Human struct {
Name string `info:"name" doc:"名字"`
Sex string `info:"性别"`
}

这个标签以 key-value 形式,使用空格分隔,可以给字段添加任意数量的 Tag

type Human struct {
Name string `info:"name" doc:"名字"`
Sex string `info:"性别"`
}

func findTag(arg interface{}) {
t := reflect.TypeOf(arg)
for i := 0; i < t.NumField(); i++ {
tag := t.Field(i).Tag.Get("info")
fmt.Println("info: ", tag)
}
}

func main() {
findTag(Human{"张三", "女"})
}

反射创建对象

快速创建一个实例

type A struct {
Name string
}

func (a *A) String() string {
return fmt.Sprintf("Hello %s \n", a.Name)
}

// 测试 unit
func main() {
val := reflect.New(reflect.TypeOf(A{})).Interface()
v := val.(*A)
fmt.Println(v.String())
}
type A struct {
Name string
}

// 测试 unit
func TestReflect(t *testing.T) {
reflectNew((*A)(nil))
}

//反射创建新对象。
func reflectNew(target interface{}) {
if target == nil {
fmt.Println("参数不能未空")
return
}

t := reflect.TypeOf(target)
if t.Kind() == reflect.Ptr { //指针类型获取真正 type 需要调用 Elem
t = t.Elem()
}

newStruc := reflect.New(t)// 调用反射创建对象
newStruc.Elem().FieldByName("Name").SetString("Lily") //设置值

newVal := newStruc.Elem().FieldByName("Name") //获取值
fmt.Println(newVal.String())
}

深度比较 DeepEqual

对于 array、slice、map、struct 等类型,想要比较两个值是否相等,不能使用 ==,处理起来十分麻烦,在对效率没有太大要求的情况下,reflect 包中的 DeepEqual 函数完美的解决了比较问题。

func DeepEqual(a1, a2 interface{}) bool

示例:

func main() {
m1 := map[int]interface{}{1: []int{1, 2, 3}, 2: 3, 3: "a"}
m2 := map[int]interface{}{1: []int{1, 2, 3}, 2: 3, 3: "a"}
if reflect.DeepEqual(m1, m2) {
fmt.Println("相等")
}
}

读取标签并赋值示例

下面利用反射实现一个简单的功能,来看看反射如何帮助我们简化代码的。

假设有一个配置类 Config,每个字段是一个配置项。为了简化实现,假设字段均为 string 类型:

type Config struct {
Name string `json:"server-name"`
IP string `json:"server-ip"`
URL string `json:"server-url"`
Timeout string `json:"timeout"`
}

配置默认从 json 文件中读取,如果环境变量中设置了某个配置项,则以环境变量中的配置为准。配置项和环境变量对应的规则非常简单:将 json 字段的字母转为大写,将 - 转为下划线,并添加 CONFIG_ 前缀。

最终的对应结果如下:

type Config struct {
Name string `json:"server-name"` // CONFIG_SERVER_NAME
IP string `json:"server-ip"` // CONFIG_SERVER_IP
URL string `json:"server-url"` // CONFIG_SERVER_URL
Timeout string `json:"timeout"` // CONFIG_TIMEOUT
}

实现这个功能非常简单,使用 switch case 或者 if else 硬编码很快就搞定了。但是,如果使用硬编码,Config 结构发生改变,例如修改 json 对应的字段,删除或新增了一个配置项,这块逻辑也需要发生改变。而更大的问题在于:容易出错,不好测试!!!

这个时候,就有了 reflect 的用武之地了。

func readConfig() *Config {
// read from xxx.json,省略
config := Config{}
typ := reflect.TypeOf(config)
value := reflect.Indirect(reflect.ValueOf(&config))

for i := 0; i < typ.NumField(); i++ {
f := typ.Field(i)
if v, ok := f.Tag.Lookup("json"); ok {
key := fmt.Sprintf("CONFIG_%s", strings.ReplaceAll(strings.ToUpper(v), "-", "_"))
if env, exist := os.LookupEnv(key); exist {
value.FieldByName(f.Name).Set(reflect.ValueOf(env))
}
}
}
return &config
}

func main() {
os.Setenv("CONFIG_SERVER_NAME", "global_server")
os.Setenv("CONFIG_SERVER_IP", "10.0.0.1")
os.Setenv("CONFIG_SERVER_URL", "example.com")
c := readConfig()
fmt.Printf("%+v", c)
}

实现逻辑其实是非常简单的:

  • 在运行时,利用反射获取到 Config 的每个字段的 Tag 属性,拼接出对应的环境变量的名称。
  • 查看该环境变量是否存在,如果存在,则将环境变量的值赋值给该字段。

运行该程序,输出为:

&{Name:global_server IP:10.0.0.1 URL:example.com Timeout:}

可以看到,环境变量中设置的三个配置项已经生效。之后无论结构体 Config 内部的字段发生任何改变,这部分代码无需任何修改即可完美的适配,出错概率也极大地降低。

使用反射修改私有变量

可以使用反射(reflection)来修改私有变量。在 Go 语言中,反射是一种强大的机制,允许在运行时检查变量的类型和值,并且可以修改私有字段的值。但是需要注意的是,反射通常不是推荐的做法,因为它会带来一些运行时的性能开销和代码可读性的降低。除非必要,最好避免在正常情况下使用反射来修改私有变量。

下面是一个示例代码,演示如何使用反射修改私有变量:

package main

import (
"fmt"
"reflect"
)

type Person struct {
name string
age int
salary float64
}

func main() {
p := Person{"John", 30, 50000.0}

// 使用反射获取 p 的值的反射对象
personReflect := reflect.ValueOf(&p).Elem()

// 获取私有字段的反射对象
// 在 Go 中,私有字段的名称会被编译器修改为包名+类型名+字段名
// 在这里,私有字段 name 被重命名为 main.Person.name
privateField := personReflect.FieldByName("name")

// 判断是否找到了私有字段
if privateField.IsValid() && privateField.CanSet() {
// 修改私有字段的值
privateField.SetString("Alice")
}

fmt.Println("Modified Person:", p)
}

在上述代码中,我们定义了一个 Person 结构体,其中的字段 name 是私有字段。使用反射,我们可以获取 Person 结构体的反射值,并通过 FieldByName 方法找到并修改私有字段 name 的值。请注意,这里仅仅是为了演示反射的使用,实际代码中应该避免过度使用反射来修改私有字段,以保持代码的健壮性和可读性。

Reference

Go语言101-反射 golang基础小记(24)——reflect.DeepEqual函数:判断两个值是否一致